Skip to content

feat(installer): add omitTypeField knob to the mcp-json family (Windsurf, #952) - #1

Closed
SEPURI-SAI-KRISHNA wants to merge 4 commits into
quickbeard:feat/custom-installer-targetsfrom
SEPURI-SAI-KRISHNA:feat/mcp-json-omit-type
Closed

feat(installer): add omitTypeField knob to the mcp-json family (Windsurf, #952)#1
SEPURI-SAI-KRISHNA wants to merge 4 commits into
quickbeard:feat/custom-installer-targetsfrom
SEPURI-SAI-KRISHNA:feat/mcp-json-omit-type

Conversation

@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown

Follow-up to your invitation on colbymchenry#1459. Adds the one knob the mcp-json family was missing for Windsurf.

What: an omitTypeField?: boolean option on the mcp-json family. When set, the server entry is written as { command, args } with no type: "stdio" key — the shape Windsurf's docs specify. Scoped exactly like your absoluteCommand knob: boolean, mcp-json family only, default false, so every existing spec is byte-identical.

Why: Windsurf (colbymchenry#952) rejects the extra type key. With this knob, Windsurf is fully expressible as a spec — no built-in needed:

{ "id": "windsurf", "displayName": "Windsurf", "family": "mcp-json", "configDir": "~/.codeium/windsurf", "configFileName": "mcp_config.json", "instructionsFileName": null, "omitTypeField": true, "notes": ["In Windsurf, open Settings → MCP (or the Cascade panel) and click \"Refresh\" to load codegraph."] }

Tests: +3 in custom-targets.test.ts (validation guard; drops type when set / keeps type:"stdio" by default; printConfig shape). Full run green: custom-targets 43/43, installer-targets 162/162.

quickbeard and others added 4 commits July 27, 2026 13:30
Adds spec-driven custom installer targets so agents without a built-in
target (opencode forks like CoDev Code colbymchenry#1272, Codex-shaped TOML agents
like Grok Build colbymchenry#1324) can be wired up with a small declarative spec
instead of a registry PR.

- Extract the opencode and codex targets into family factories
  (opencode-family.ts, toml-family.ts) — behavior-preserving, pinned by
  the existing 162-test installer contract suite. Add a third generic
  mcp-json family for standard mcpServers-JSON agents.
- targets/custom.ts: spec schema + validation (id shape, reserved
  words, builtin collisions, path-shape rules), tolerant loader
  (invalid specs skip with a warning, never crash the installer),
  strict add/remove persistence to ~/.codegraph/targets.json
  (CODEGRAPH_TARGETS_FILE override).
- Registry merges customs after built-ins; --target <id>/auto/all, the
  interactive multiselect, uninstall's sweep, and --print-config all
  see them with no further changes.
- CLI: codegraph targets add|list|remove.
- 30 new tests in __tests__/custom-targets.test.ts; design doc at
  docs/design/custom-installer-targets.md; CHANGELOG entry.
Custom specs can now carry post-install notes the orchestrator surfaces
verbatim (the existing WriteResult.notes channel, same as Cursor's
'Restart Cursor to apply') — for agent quirks the user must act on,
e.g. Windsurf not reloading mcp_config.json until the MCP panel's
Refresh is hit. Install-only (including idempotent re-runs); never
written into any agent file; validated to ≤5 single-line strings.

Suggested by @SEPURI-SAI-KRISHNA in colbymchenry#1459 review discussion (the
Windsurf refresh gotcha from colbymchenry#952).
…specs

Two knobs for GUI-IDE agents, both from Qoder requirements confirmed by
its target-PR author in colbymchenry#1459 discussion (colbymchenry#1277):

- configDir (toml/mcp-json families) now accepts a per-platform
  {darwin, win32, linux} map, and either form may start with one
  leading ${ENV} token (${APPDATA}/... — Windows has no ~ for
  %APPDATA%-rooted dirs). Unresolvable on this machine (no entry for
  the platform, env var unset) degrades to not-installed / a 'cannot
  locate config dir' note — never a crash.
- absoluteCommand (mcp-json family) writes the codegraph binary's
  resolved absolute path as the entry command, for Dock/Finder-launched
  macOS apps whose stripped PATH can't find nvm-managed binaries.
  Reuses the built-in Antigravity target's resolver, now extracted to
  targets/shared.ts (antigravity.ts imports it; behavior unchanged).

expandHomeDir moved from toml-family.ts into the new spec-paths.ts
alongside the platform-map/env-token resolution.

@quickbeard quickbeard left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed and tested locally — this is clean and I'd like to take it. Thank you for turning it around so fast.

Scope is exactly right. omitTypeField mirrors absoluteCommand one-for-one: boolean, mcp-json family only (with the same validation guard and error message), default false, so every existing spec serializes byte-identically. No existing target changes shape.

Correctness checks I ran, all good:

  • serverEntry() mutating then delete entry.type is safe — getMcpServerConfig() returns a fresh object per call, so there's no shared-state aliasing (same reason absoluteCommand's entry.command = … is safe).
  • Idempotency holds across a toggle: installing without the knob writes {type, command, args}; re-installing the same target with omitTypeField: true makes after = {command, args}, so jsonDeepEqual(before, after) is false → one clean updated, not a spurious unchanged. And uninstall deletes the codegraph key regardless of entry shape, so it still reverses.
  • printConfig reflects the knob for free because it already routes through serverEntry() — your second test pins that.
  • Windsurf's global-only behavior falls out correctly from the spec omitting localConfigDir (family returns supportsLocation('local') === false), so no local-install surprise.

Tests: the 3 you added match the existing absoluteCommand / notes patterns exactly (validation guard, drop-vs-keep, printConfig shape). I ran the full installer set on a merge of this onto the branch tip: 208 passing, tsc clean.

One tiny optional note, non-blocking: a spec that sets both absoluteCommand and omitTypeField isn't covered by a test, but they're independent one-liners in serverEntry() and each is tested alone, so I'm not worried.

Heads-up on mechanics: I've just rebased colbymchenry#1459 onto latest main (75 commits had landed). To spare you a rebase of this PR onto the moved tip, I'll bring your commit in via cherry-pick with your authorship preserved and reference it here — you'll show up as the commit author on colbymchenry#1459. Flagging so the "closed" status on this PR reads as "included," not "rejected." Thanks again — this is the piece that makes Windsurf fully spec-expressible.

@quickbeard
quickbeard force-pushed the feat/custom-installer-targets branch from d01359c to b657797 Compare August 7, 2026 10:14
quickbeard pushed a commit that referenced this pull request Aug 7, 2026
…ame-collision matches (CG-10, colbymchenry#1500)

Explore's per-file relevance awarded +50/+10/+3/+1 by match class and admitted
anything scoring >= 3. Neither half held up: the tier said HOW a symbol reached
us, never whether the match was evidence, and an absolute floor admits noise on
any repo where the top file scores 50+. Three scripts/agent-eval/*.mjs harnesses
took 63% of this repo's own "how does explore allocate its output budget" answer
on nothing but an unused `const explore` and a `const BUDGET`.

Four levers:

- KIND WEIGHT (RELEVANCE_KIND_WEIGHT): callables and types 1.0, members ~0.5,
  variable/constant/parameter 0.15-0.35. A weak-kind symbol with no usage edge
  anywhere in the graph (`contains` excluded — nesting is not usage) drops to
  0.08. Only weak kinds in the top two tiers pay for the DB probe; the subgraph's
  own edges answer most cases free. No measurable latency change (210 vs 211
  ms/call, n=12 interleaved).

- PERIPHERAL CAP: nodes >=2 hops from any match accumulate into a bucket capped
  at 5. Uncapped they added a flat +1 each, so a file grew more relevant by being
  bigger — parse-session.mjs reached 22 off one constant plus twelve unrelated
  symbols.

- RANK PENALTY: generated files x0.3, low-value x0.5, applied to the score AND
  the graph mass. Score alone would not have fixed colbymchenry#1500 — the generated CRUD
  carries MORE graph mass than the hand-written use-case, and graph mass outranks
  score in the comparator. Self-normalizing, never a hard exclusion.

- RELATIVE FLOOR: clamp(topScore * 0.2, 1, 10). Capped at one full-strength
  direct match so concentration elsewhere can never exclude one (without it a
  named-seed-heavy file pushed the floor to 21 and dropped a file the agent had
  named by class name). Backfills to 3 candidates when it would leave fewer, and
  drops the evidence requirement rather than return nothing at all.

excludeLowValueFiles was dead config — declared per tier, read nowhere; the
test/spec exclusion has been unconditional for a while. Removed. The real gap was
the detector: `isLowValue` anchored on a leading `/`, so a repo-ROOT `test/` dir
(express, cobra, most of npm and Go) never matched — express's routing question
spent 59% of its envelope on three test files. Anchored at `^` too, and the
filter now runs before the floor and judges "are there other candidates?" on the
whole gather.

Measured before/after on the same indexes (baseline bd86ad2):
- payroll-go fixture: generated 57.4% -> 23.5%; answer 25.6% -> 61.5%; cycle.go
  delivered 0 -> 38.9%. Generated ranks colbymchenry#3/colbymchenry#4, was #1/colbymchenry#2.
- self-query fixture: eval scripts 72% -> 0%; tools.ts ranks #1.
- express "route a request": 59% to test/* -> lib/application.js + lib/response.js
- cobra x3, codegraph "indexing pipeline": byte-identical (control)

Diagnostic gains a per-file penalty multiplier and NodeKind mix, so "why did this
file score X" is legible. Selection stages reordered to match the pipeline.

CG-6's gates flip from it.fails to live regressions except the byte-split ones,
which stay open for CG-12 (allocation still follows file size within the ranked
set).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
quickbeard pushed a commit that referenced this pull request Aug 7, 2026
…(CG-27)

CG-27 asked whether the >50%-of-file envelope drop should cover `function` /
`method`, so a `createFoo()` factory returning an object of closures stops
merging every closure inside it into one cluster. Measured on a hermetic
fixture, it should not, and the issue is closed as obsolete with CG-30 credited.

Two mechanisms already absorb the shape. shrinkCluster orders members by
(importance desc, size ASC) and refuses any member that overruns the cap once
something is kept, so a file-spanning member is only selected when it is the
sole member of the top importance tier — eight of nine query shapes never
selected it at all. When it IS selected, CG-30 windows it on whole lines, so
the file still delivers bounded, readable source (6 of 9 closure definitions
in that configuration).

Dropping the range instead SPLITS the file, and only the first-chosen cluster
may be shrunk: a trivial 7-line cluster won the density tiebreak and the
answer-bearing cluster was dropped whole — rank-#1 file 7,539 chars and 7 of 11
closures to 397 and none. Reaching the same intent more carefully (defer the
envelope MEMBER inside shrinkCluster, leaving clustering untouched) is noise:
69 vs 68 closure definitions across nine query shapes. Nothing shipped.

Adds the fixture, the probe, a standing gate on the outcome, and the record —
including a real defect the measurement exposed on the epic tip: django's
query.py leaves 8,212 of 10,135 unspent and drops a score-290 cluster to keep a
score-14 one. Filed separately.

No behaviour change, so no CHANGELOG entry.
quickbeard pushed a commit that referenced this pull request Aug 7, 2026
CG-27 proposed adding function/method to ENVELOPE_KINDS so a factory closure
spanning most of its file stops merging every inner symbol into one cluster.
The issue required the ranking claim be measured before any fix. It was, on a
hermetic fixture built to make the pattern maximally visible, and it does not
hold — nothing shipped to src/.

The literal change is a large regression: dropping the enclosing range SPLITS
the file into a trivial cluster (a type alias plus a helper, span 7) and the
answer-bearing one (every closure, span 359). Cluster ranking breaks the equal
maxImportance tie on density, so the trivial cluster wins, is taken first, and
is the only one that may be shrunk; the answer-bearing cluster then does not fit
and is dropped whole. Rank #1 fell from 7,539 delivered chars to 397, and from
7 of 11 inner closures to 0. The enclosing range was holding the file together
as one cluster, inside which shrinkCluster already did the per-symbol ranking
the issue asked for.

A better mechanism reaching the same intent — deferring the envelope member
inside shrinkCluster, leaving clustering untouched — is noise: 69 vs 68 inner
definitions across nine query shapes, one better, one worse, seven unchanged.

The one configuration where the envelope IS selected (the factory as sole
top-tier member) is already absorbed by CG-30, which windows it on whole lines:
a contiguous readable head carrying 6 of 9 closures, bounded and never empty.

Kept: the fixture, the deterministic probe, and the measurement record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
quickbeard pushed a commit that referenced this pull request Aug 7, 2026
A file that declares nothing but types and that nothing in the index depends
on — a hand-written ambient `.d.ts` of global shims, vendored typings, module
augmentation — cannot answer a flow question: no bodies, no call edges, no
behaviour, nothing typed by it. But the identifiers it declares are exactly the
generic ones a prose question uses (`Body`, `Message`, `ImageMetadata`,
`ReadableStream`), so on term overlap it out-scored the implementation. Measured
on the new fixture: rank #1 and 51% of delivered source, with the flow's own
entry file pushed out of the response entirely.

Measured first, per the issue: the Wrangler `worker-configuration.d.ts` that
opened this is already handled by CG-25's banner detection, worth 15-46 points
of envelope share across four flow queries. CG-25 credited; only the un-bannered
case needed anything.

`rankPenalty` now multiplies score and graph mass by 0.5 for such files, taken
as the STRONGER of it and the generated penalty rather than multiplied — one
property two signals see must not be charged twice. Detection is structural, not
by extension, and four conditions deep. Two of them were forced by measurement:
requiring every symbol to be type-level takes the corpus flag rate from 1-18%
(which swept in Kotlin sealed classes, Rust mod.rs re-exports and django's
locale tables) down to 0-4%; requiring that nothing depends on the file
separates an ambient shim from a working types module, and without it the rule
demoted displacement-ts's pipeline `types.ts` and broke the CG-31 gate.

A query that NAMES a declared type is exempt, so a question about a type still
reaches its declaration at full weight. Precise tokens only, so "…the file
body…" cannot exempt a `Body` interface it never meant to name; this needs its
own set because `namedSeedIds` is callable-only and a type never becomes one.

Regression evidence in docs/benchmarks/explore-declaration-only-cg28.md:
6-repo envelope sweep byte-identical against a clean baseline build, zero
ambient files reach the candidate set on VS Code across five queries, corpus
flag rate 0-0.74%, both allocation fixtures PASS, full suite 2,978 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
quickbeard pushed a commit that referenced this pull request Aug 7, 2026
…CG-28)

Both halves of the issue were measured on a hermetic fixture of four
declaration-shaped files varying on banner and depended-on-ness.

CG-25 already handles the motivating file: the Wrangler worker-configuration.d.ts
that opened this issue is demoted by the generated penalty alone, worth 15-46
points of envelope share across four flow queries. No new mechanism for it.

The narrower gap is real. A declaration file with NO banner carried pen 1.00,
took rank #1 and 51% of delivered source on a prose flow query, and displaced
the flow's own entry file out of the response entirely.

The rule is deliberately narrow, and both conditions were derived by survey
rather than guessed. 'Declares no callable and calls nothing' flags 1.1-18.0%
of files across the corpus and catches real source — okhttp's SocketPolicy.kt,
tokio/src/runtime/mod.rs, Alamofire's umbrella file, django's locale format
tables. Requiring every symbol to be type-level drops that to 0-4%. The
'nothing depends on it' condition was added after the broader version demoted a
pure-interface file with 13 inbound imports and broke the CG-31 displacement
gate — a different invariant entirely.

Does NOT stack with the generated penalty: rankPenalty takes Math.min of the
two, so a file that is both takes the stronger, never the product. A query that
NAMES a declaration symbol exempts its file entirely, so asking about a type
still reaches it at full weight.

Six-repo envelope is byte-identical to the pre-change tip — the rule does not
fire on any benchmark repo, consistent with the 0-4% survey.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@quickbeard

Copy link
Copy Markdown
Owner

Included in the branch as b657797 — your commit, your authorship, cherry-picked verbatim on top of the rebase onto latest main. It's now part of colbymchenry#1459 (full suite green: 2,858 passing). Closing this as included, not rejected — GitHub can't auto-detect the merge because the rebase changed the surrounding SHAs. Thank you for the clean contribution; Windsurf is now fully spec-expressible.

For anyone landing here later, the Windsurf spec:

{ "id": "windsurf", "displayName": "Windsurf", "family": "mcp-json",
  "configDir": "~/.codeium/windsurf", "configFileName": "mcp_config.json",
  "instructionsFileName": null, "omitTypeField": true,
  "notes": ["In Windsurf, open the MCP panel and click Refresh to load codegraph."] }

@quickbeard quickbeard closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants